﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MBTQuotesFeedPlugin.cs" company="Open Trader">
//   Copyright (c) David Denis (david.denis@systemathics.com)
// </copyright>
// <summary>
//   |  Open Trader - The Open Source Systematic Trading Platform
//   |
//   |  This program is free software: you can redistribute it and/or modify
//   |  it under the terms of the GNU General Public License as published by
//   |  the Free Software Foundation, either version 2 of the License, or
//   |  (at your option) any later version.
//   |
//   |  This program is distributed in the hope that it will be useful,
//   |  but WITHOUT ANY WARRANTY; without even the implied warranty of
//   |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   |  GNU General Public License for more details.
//   |
//   |  You should have received a copy of the GNU General Public License
//   |  along with this program.  If not, see http://www.gnu.org/licenses
//   |
//   |  Up to date informations about Open Trader can be found at :
//   |    http://opentrader.org
//   |    http://opentrader.codeplex.com
//   |
//   |  For professional services, please visit us at :
//   |    http://www.systemathics.com
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Org.OpenTrader.Framework.Forge.Plugins
{
    #region Using Directives

    using System;
    using System.Collections.Generic;
    using System.Linq;

    using Org.OpenTrader.Framework.Enums;
    using Org.OpenTrader.Framework.Events;
    using Org.OpenTrader.Framework.Forge.Enums;
    using Org.OpenTrader.Framework.Forge.Events;
    using Org.OpenTrader.Framework.Forge.Interfaces;
    using Org.OpenTrader.Framework.LiveObjects;
    using Org.OpenTrader.Framework.LiveObjects.Interfaces;

    #endregion

    /// <summary>
    /// The mbt quotes feed plugin.
    /// </summary>
    [Credits("MBT Feed Plugin", "Handles feed operations using MBT Quotes API (Message Oriented)", "David Denis", 
        "Copyright (C) David Denis <mailto:david.denis@systemathics.com>")]
    [ImplementsInterface("Org.OpenTrader.Framework.Forge.Interfaces.IFeedPlugin")]
    [ConfigurationField("Host", typeof(ConfigurationField.String), "Hostname to connect to", IsMandatory = true, DefaultValue = "216.52.236.111")]
    [ConfigurationField("Port", typeof(ConfigurationField.Int), "Port to connect to", IsMandatory = true, DefaultValue = 5020)]
    [ConfigurationField("User", typeof(ConfigurationField.String), "User under which to connect", IsMandatory = true, DefaultValue = "DEMOXDZK")]
    [ConfigurationField("Password", typeof(ConfigurationField.String), "Password of the user", IsMandatory = true, DefaultValue = "1book2mice")]
    [ConfigurationField("UseMarketParticipants", typeof(ConfigurationField.Bool), "Use market participants", IsMandatory = true, DefaultValue = false)]
    [ConfigurationField("SubscriptionConfigurationSchema", "Symbol", typeof(ConfigurationField.String), "Symbol needed to register", IsMandatory = true, 
        DefaultValue = "SYMBOL")]
    public sealed class MBTQuotesFeedPlugin : FeedPlugin
    {
        #region Constants and Fields

        /// <summary>
        /// The ask field number
        /// </summary>
        private const int Ask = 20011;

        /// <summary>
        /// The best ask field number
        /// </summary>
        private const int BestAskPrice = 2004;

        /// <summary>
        /// The best ask size field number
        /// </summary>
        private const int BestAskSize = 2006;

        /// <summary>
        /// The best bid price field number
        /// </summary>
        private const int BestBidPrice = 2003;

        /// <summary>
        /// The best bid size field number
        /// </summary>
        private const int BestBidSize = 2005;

        /// <summary>
        /// The bid field number
        /// </summary>
        private const int Bid = 20010;

        /// <summary>
        /// The close field number
        /// </summary>
        private const int Close = 2008;

        /// <summary>
        /// The exchange field number
        /// </summary>
        private const int Exchange = 2022;

        /// <summary>
        /// The hi field number
        /// </summary>
        private const int High = 2009;

        /// <summary>
        /// The last price field number
        /// </summary>
        private const int LastPrice = 2002;

        /// <summary>
        /// The last size field number
        /// </summary>
        private const int LastSize = 2007;

        /// <summary>
        /// The low field number
        /// </summary>
        private const int Low = 2010;

        /// <summary>
        /// The mmid size field number
        /// </summary>
        private const int MmidSize = 2019;

        /// <summary>
        /// The mmid source field number
        /// </summary>
        private const int MmidSource = 2033;

        /// <summary>
        /// The mmid status field number
        /// </summary>
        private const int MmidStatus = 2024;

        /// <summary>
        /// The mmid time field number
        /// </summary>
        private const int MmidTime = 2020;

        /// <summary>
        /// The mpid field number
        /// </summary>
        private const int Mpid = 2016;

        /// <summary>
        /// The open field number
        /// </summary>
        private const int Open = 2010;

        /// <summary>
        /// The quote price field number
        /// </summary>
        private const int QuotePrice = 2018;

        /// <summary>
        /// The quot side field number
        /// </summary>
        private const int QuoteSide = 2017;

        /// <summary>
        /// The symbol field number
        /// </summary>
        private const int Symbol = 1003;

        /// <summary>
        /// The tick condition field number
        /// </summary>
        private const int TickCondition = 2082;

        /// <summary>
        /// The tick status field number
        /// </summary>
        private const int TickStatus = 2083;

        /// <summary>
        /// The tick type field number 
        /// </summary>
        private const int TickType = 2084;

        /// <summary>
        /// The time and sales type field number
        /// </summary>
        private const int TimeAndSalesType = 2039;

        /// <summary>
        /// The time stamps field number (level 1)
        /// </summary>
        private const int TimeStamp = 2014;

        /// <summary>
        /// The identity.
        /// </summary>
        private static readonly Identity Identity = Identity.Create(System.Reflection.MethodBase.GetCurrentMethod());

        /// <summary>
        /// The numbers.
        /// </summary>
        private static readonly char[] numbers = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

        /// <summary>
        /// The prettyPrint.
        /// </summary>
        private static readonly IDictionary<int, string> prettyPrint;

        /// <summary>
        /// The handlers store.
        /// </summary>
        private readonly HandlersStore handlersStore = new HandlersStore();

        /// <summary>
        /// The subscription mapper.
        /// </summary>
        private readonly SubscriptionMappingStore<string> subscriptionMappingStore = new SubscriptionMappingStore<string>();

        /// <summary>
        /// The Connection.
        /// </summary>
        private MBTQuotesConnection connection;

        /// <summary>
        /// The useMarketParticipants.
        /// </summary>
        private bool useMarketParticipants;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes static members of the <see cref="MBTQuotesFeedPlugin"/> class.
        /// </summary>
        static MBTQuotesFeedPlugin()
        {
            prettyPrint = new Dictionary<int, string>();
            prettyPrint[Symbol] = "Symbol";
            prettyPrint[Mpid] = "Mpid";
            prettyPrint[QuoteSide] = "QuoteSide";
            prettyPrint[QuotePrice] = "QuotePrice";
            prettyPrint[MmidSize] = "MmidSize";
            prettyPrint[MmidTime] = "MmidTime";
            prettyPrint[MmidStatus] = "MmidStatus";
            prettyPrint[MmidSource] = "MmidSource";
            prettyPrint[Bid] = "Bid";
            prettyPrint[Ask] = "Ask";
            prettyPrint[LastPrice] = "LastPrice";
            prettyPrint[LastSize] = "LastSize";
            prettyPrint[BestBidPrice] = "BestBidPrice";
            prettyPrint[BestBidSize] = "BestBidSize";
            prettyPrint[BestAskPrice] = "BestAskPrice";
            prettyPrint[BestAskSize] = "BestAskSize";
            prettyPrint[High] = "High";
            prettyPrint[Low] = "Low";
            prettyPrint[Open] = "Open";
            prettyPrint[Close] = "HardOrSoftClosed";
            prettyPrint[TimeStamp] = "TimeStamp";
            prettyPrint[Exchange] = "Exchange";
            prettyPrint[TimeAndSalesType] = "TimeAndSalesType";
            prettyPrint[TickCondition] = "TickCondition";
            prettyPrint[TickStatus] = "TickStatus";
            prettyPrint[TickType] = "TickType";
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MBTQuotesFeedPlugin"/> class.
        /// </summary>
        /// <param name="descriptor">
        /// The descriptor.
        /// </param>
        /// <param name="container">
        /// The container.
        /// </param>
        public MBTQuotesFeedPlugin(ILiveObjectDescriptor descriptor, ILiveObjectContainer container)
            : base(descriptor, container)
        {
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets ConnectionStatus.
        /// </summary>
        public override EConnectionStatus ConnectionStatus
        {
            get
            {
                return this.connection.ConnectionStatus;
            }
        }

        /// <summary>
        /// Gets ConnectionURL.
        /// </summary>
        public override string ConnectionURL
        {
            get
            {
                return this.connection.ToString();
            }
        }

        /// <summary>
        /// Gets EmitedEventsType.
        /// </summary>
        public override EEmitedEventsType EmitedEventsType
        {
            get
            {
                return EEmitedEventsType.QuoteEvents;
            }
        }

        /// <summary>
        /// Gets a value indicating whether NeedResubscriptionIfDisconnected.
        /// </summary>
        public override bool NeedResubscriptionIfDisconnected
        {
            get
            {
                return true;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// The connect.
        /// </summary>
        /// <returns>
        /// </returns>
        public override Status Connect()
        {
            this.subscriptionMappingStore.Clear();
            this.handlersStore.Clear();

            return this.connection.Connect();
        }

        /// <summary>
        /// The disconnect.
        /// </summary>
        /// <returns>
        /// </returns>
        public override Status Disconnect()
        {
            this.subscriptionMappingStore.Clear();
            this.handlersStore.Clear();

            return this.connection.Disconnect();
        }

        /// <summary>
        /// The on activation context.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="activationEventArgs">
        /// The activation Context.
        /// </param>
        public override void OnActivation(object sender, ActivationEventArgs activationEventArgs)
        {
            if (activationEventArgs is ActivationEventArgs.CreateDestroy)
            {
                switch (activationEventArgs.Type)
                {
                    case EActivationType.Activation:
                        this.subscriptionMappingStore.Clear();
                        this.useMarketParticipants = (bool)this.Configuration["UseMarketParticipants"];
                        this.connection = new MBTQuotesConnection(
                            this.Configuration["Host"], this.Configuration["Port"], this.Configuration["User"], this.Configuration["Password"], this);
                        break;

                    case EActivationType.Desactivation:
                        this.subscriptionMappingStore.Clear();
                        this.handlersStore.Clear();
                        break;
                }
            }
        }

        /// <summary>
        /// The process input.
        /// </summary>
        /// <param name="input">
        /// The input.
        /// </param>
        public void ProcessInput(string input)
        {
            var messages = input.Split('\n');

            foreach (var message in messages)
            {
                // Jump empty lines
                if (message.Length == 0)
                {
                    continue;
                }

                var level = '?';
                if (message.StartsWith("1|"))
                {
                    level = '1';
                }

                if (message.StartsWith("2|"))
                {
                    level = '2';
                }

                if (message.StartsWith("3|"))
                {
                    level = '3';
                }

                if (message.StartsWith("N|"))
                {
                    level = 'N';
                }

                // Unknown
                if (level == '?')
                {
                    Stdout.WriteLine("Excluded: " + message);
                    continue;
                }

                // construct a map of int->string
                var fields = message.Substring(2).Split(';');
                IDictionary<int, string> fieldsMap = new Dictionary<int, string>();
                foreach (var pair in fields)
                {
                    object[] objects = pair.Split('=');
                    if (objects.Length == 2)
                    {
                        try
                        {
                            var fieldNumber = int.Parse((string)objects[0]);
                            var fieldContent = (string)objects[1];
                            fieldsMap[fieldNumber] = fieldContent;
                        }
                        catch (Exception)
                        {
                        }
                    }
                }

                // Symbol is not there ? OnStop
                if (!fieldsMap.ContainsKey(Symbol))
                {
                    Stdout.WriteLine("Excluded: " + message);
                    continue;
                }

                // No (or no more) in subscription list
                if (!this.subscriptionMappingStore.HasMapping(fieldsMap[Symbol]))
                {
                    Stdout.WriteLine("Excluded: " + message);
                    continue;
                }

                // The unique key for the dispatcher (several instruments may have the same wiring (same plugin and same parameters))
                // Use only one dispatcher, those hypothetical instruments are subscribers of the dispatcher
                var feedWiring = this.subscriptionMappingStore.GetMapping(fieldsMap[Symbol]);

                // Here it the handler
                var handler = this.handlersStore.GetHandler(feedWiring);

                // Decode and route to "MarketField" or "MarketBook" feed pipelines
                switch (level)
                {
                    case '1':
                        {
                            IList<MarketField> updates = new List<MarketField>();
                            MarketField update = null;

                            // Close
                            if (fieldsMap.ContainsKey(Close))
                            {
                                update = new MarketField(EMarketField.Close, double.Parse(fieldsMap[Close], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // Open
                            if (fieldsMap.ContainsKey(Open))
                            {
                                update = new MarketField(EMarketField.Open, double.Parse(fieldsMap[Open], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // High
                            if (fieldsMap.ContainsKey(High))
                            {
                                update = new MarketField(EMarketField.High, double.Parse(fieldsMap[High], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // Low
                            if (fieldsMap.ContainsKey(Low))
                            {
                                update = new MarketField(EMarketField.Low, double.Parse(fieldsMap[Low], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // Last
                            if (fieldsMap.ContainsKey(LastPrice))
                            {
                                update = new MarketField(EMarketField.Last, double.Parse(fieldsMap[LastPrice], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // Last Size
                            if (fieldsMap.ContainsKey(LastSize))
                            {
                                update = new MarketField(EMarketField.LastSize, int.Parse(fieldsMap[LastSize], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // BEST Bid
                            if (fieldsMap.ContainsKey(BestBidPrice))
                            {
                                update = new MarketField(EMarketField.BestBid, double.Parse(fieldsMap[BestBidPrice], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // BEST Bid Size
                            if (fieldsMap.ContainsKey(BestBidSize))
                            {
                                update = new MarketField(EMarketField.BestBidSize, int.Parse(fieldsMap[BestBidSize], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // BEST Ask
                            if (fieldsMap.ContainsKey(BestAskPrice))
                            {
                                update = new MarketField(EMarketField.BestAsk, double.Parse(fieldsMap[BestAskPrice], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // BEST Ask Size
                            if (fieldsMap.ContainsKey(BestAskSize))
                            {
                                update = new MarketField(EMarketField.BestAskSize, int.Parse(fieldsMap[BestAskSize], Constants.CultureInfo));
                                updates.Add(update);
                            }

                            // push events
                            if (updates.Count > 0)
                            {
                                this.RaiseEventMarketFieldCells(updates.ToArray(), false, handler);
                            }
                        }

                        break;

                    case '2':
                        {
                            if (!fieldsMap.ContainsKey(Mpid))
                            {
                                Stdout.WriteLine("Filtered: {0} (no Mpid)", this.PrettyPrint(message));
                                return;
                            }

                            var quoteSize = 0;
                            double quotePrice = 0;
                            var closed = false;

                            if (fieldsMap.ContainsKey(MmidSize))
                            {
                                Devel.NoThrow(delegate { quoteSize = int.Parse(fieldsMap[MmidSize], Constants.CultureInfo); });
                            }

                            if (fieldsMap.ContainsKey(QuotePrice))
                            {
                                Devel.NoThrow(delegate { quotePrice = double.Parse(fieldsMap[QuotePrice], Constants.CultureInfo); });
                            }

                            if (fieldsMap.ContainsKey(MmidStatus))
                            {
                                closed = !fieldsMap[MmidStatus].Equals("O");
                            }

                            if (quoteSize == 0)
                            {
                                closed = true;
                            }

                            if (quotePrice == 0)
                            {
                                closed = true;
                            }

                            // Normalize the MMID to something suitable for book reconstruction
                            // Some received MMID are not "unique key" so make this happen in such cases
                            // Wanted format is something like XXXX:YYYYYYYY where XXXX is the constributor and XXXX:YYYYYYYY a unique key for a quote
                            var mmid = fieldsMap[Mpid];

                            // Forex quotes (All Levels)
                            if (mmid.IndexOf("TDFX") >= 0)
                            {
                                mmid = mmid.Replace("TDFX", "TDFX:");
                            }
                            else if (mmid.IndexOfAny(numbers) < 0)
                            {
                                // One Level only quote type workaround, those quotes doesnt contain numbers :>
                                if (!this.useMarketParticipants)
                                {
                                    // Stdout.WriteLine("Filtered: {0} (market participant)", PrettyPrint(message));
                                    return;
                                }

                                // Just add a "side" to make it a unique key, those participants can only contribute once on each side
                                mmid = string.Format("{0}:00000000{1}", mmid, fieldsMap[QuoteSide]);
                            }

                            var pos = mmid.IndexOf(':');
                            if (pos <= 0)
                            {
                                // Normalization failed
                                Stdout.WriteLine("Filtered: {0} (unable to normalize Mpid)" + this.PrettyPrint(message));
                                return;
                            }

                            var quoteKey = mmid;
                            var quoteContributor = mmid.Substring(0, pos);
                            var quoteSide = int.Parse(fieldsMap[QuoteSide]) == Bid ? ESide.Bid : ESide.Ask;

                            if (closed)
                            {
                                // is a "closing" quote
                                this.RaiseEventMarketQuotes(new[] { MarketQuote.ClosingQuote(quoteKey, Timings.Now, quoteSide) }, false, handler);
                            }
                            else
                            {
                                // is a "non closing"
                                IContribution contribution = new Contribution(quoteKey, quoteContributor, quoteSize);
                                this.RaiseEventMarketQuotes(
                                    new[] { MarketQuote.Quote(quoteKey, Timings.Now, quoteSide, quotePrice, quoteSize, contribution) }, false, handler);
                            }
                        }

                        break;

                        ////case '3':
                        ////    {
                        ////        IList<MarketField> updates = new List<MarketField>();
                        ////        MarketField update = null;

                        ////        // TRADE
                        ////        if (fieldsMap.ContainsKey(TickType) && fieldsMap[TickType] == "0" /* Trade */)
                        ////        {
                        ////            update = MarketField.Trade(
                        ////                Timings.Now, double.Parse(fieldsMap[LastPrice], Constants.CultureInfo), int.Parse(fieldsMap[LastSize]));
                        ////            updates.Add(update);
                        ////        }

                        ////        // push events
                        ////        if (updates.Count > 0)
                        ////        {
                        ////            this.RaiseEventMarketFieldCells(updates.ToArray(), false, handler);
                        ////        }
                        ////    }

                        ////break;
                    case 'N':

                        break;
                }
            }
        }

        /// <summary>
        /// Send a "start subscription" request.
        /// </summary>
        /// <param name="wiring">
        /// The wiring.
        /// </param>
        /// <param name="handler">
        /// The handler.
        /// </param>
        /// <returns>
        /// True on success, False otherwise
        /// </returns>
        public override Status StartSubscription(IFeedWiring wiring, RemotableEventHandler<FeedEventArgs> handler)
        {
            // No Connection available
            if (this.connection.ConnectionStatus != EConnectionStatus.Connected)
            {
                return Status.Failure(Identity, "StartSubscription: Connection not opened");
            }

            // Already some subscription
            if (this.subscriptionMappingStore.HasMapping(wiring))
            {
                return Status.Failure(Identity, "StartSubscription: Already subscribed");
            }

            // What symbol to subscribe
            var symbol = wiring.SubscriptionConfiguration["Symbol"];
            if (symbol == null)
            {
                return Status.Failure(Identity, "StartSubscription: 'Symbol' parameter is null");
            }

            // Add request entries
            this.subscriptionMappingStore.SetMapping(wiring, symbol);

            // Keep track of handler
            this.handlersStore.SetHandler(wiring, handler);

            // Fundamentals
            this.connection.SendData(string.Format("H|{0}={1}", Symbol, symbol));

            // Level One & Level Two
            this.connection.SendData(string.Format("S|{0}={1};2000=20002", Symbol, symbol));

            // All done
            return Status.Success(Identity, "StartSubscription: Subscription message sent");
        }

        /// <summary>
        /// Send a "stop subscription" request.
        /// </summary>
        /// <param name="wiring">
        /// The wiring.
        /// </param>
        /// <param name="handler">
        /// The handler.
        /// </param>
        /// <returns>
        /// True on sucess, False otherwise
        /// </returns>
        public override Status StopSubscription(IFeedWiring wiring, RemotableEventHandler<FeedEventArgs> handler)
        {
            // No Connection available
            if (this.connection.ConnectionStatus != EConnectionStatus.Connected)
            {
                return Status.Failure(Identity, "StopSubscription: Connection not opened");
            }

            // Not subscribed
            if (!this.subscriptionMappingStore.HasMapping(wiring))
            {
                return Status.Failure(Identity, "StopSubscription: Not subscribed");
            }

            // Symbol 
            var symbol = wiring.SubscriptionConfiguration["Symbol"];

            // Unsubscribe
            this.connection.SendData(string.Format("U|{0}={1};2000=20002", Symbol, symbol));

            // Remove requests
            this.subscriptionMappingStore.UnsetMapping(wiring, symbol);

            // Remove handler
            this.handlersStore.UnsetHandler(wiring);

            // All done
            return Status.Success(Identity, "StopSubscription: Unsubscription message sent");
        }

        #endregion

        #region Methods

        /// <summary>
        /// Pretty print fields, that is replace tag number by tag names
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <returns>
        /// The pretty print.
        /// </returns>
        private string PrettyPrint(string message)
        {
            foreach (var field in prettyPrint)
            {
                message = message.Replace(string.Empty + field.Key, field.Value);
            }

            return message.TrimEnd(new[] { '\n' });
        }

        #endregion
    }
}